#include <glload/gl_3_3.h>
//#include <glload/gll.hpp>
#include <GL/freeglut.h>
#include <glutil/Shader.h>
#include <assert.h>
#include <math.h>
#include <fstream>
#include <sstream>
#include <string>
#include <algorithm>
//##include "Shader.h"
#include "Renderer.h"

#define ARRAY_COUNT( array ) (sizeof( array ) / (sizeof( array[0] ) * (sizeof( array ) != sizeof(void*) || sizeof( array[0] ) <= sizeof(void*))))

//
//
//
Renderer::Renderer()
{
}


//
//
//
Renderer::~Renderer()
{
}


//
//
//
void Renderer::Initialize()
{
#define COLOR_RED 1.0f, 0.0f, 0.0f, 1.0f
#define COLOR_GREEN 0.0f, 1.0f, 0.0f, 1.0f
#define COLOR_BLUE 0.0f, 0.0f, 1.0f, 1.0f
#define COLOR_YELLOW 1.0f, 1.0f, 0.0f, 1.0f
#define COLOR_PURPLE 0.0f, 1.0f, 1.0f, 1.0f
#define COLOR_WHITE 1.0f, 1.0f, 1.0f, 1.0f

	// Create program
	std::vector<GLuint> shaders;
	shaders.push_back(this->LoadShader(GL_VERTEX_SHADER, "Shaders\\simple.vert"));
	shaders.push_back(this->LoadShader(GL_FRAGMENT_SHADER, "Shaders\\simple.frag"));
	program = this->CreateProgram(shaders);

	std::for_each(shaders.begin(), shaders.end(), glDeleteShader);
	
	// Setup shaders
	glUseProgram(program);
	offsetUniform = glGetUniformLocation(program, "offset");
	GLuint perspectiveMatrixUnif = glGetUniformLocation(program, "perspectiveMatrix");
	float theMatrix[16];
	memset(theMatrix, 0, sizeof(float)*16);

	float fFrustumScale = 1.0f; float fzNear = 0.5f; float fzFar = 3.0f;
	theMatrix[0] = fFrustumScale / ((float)windowWidth / (float)windowHeight);
	theMatrix[5] = fFrustumScale;
	theMatrix[10] = (fzFar + fzNear) / (fzNear - fzFar);
	theMatrix[14] = (2*fzFar*fzNear) / (fzNear - fzFar);
	theMatrix[11] = -1.0f;

	glUniformMatrix4fv(perspectiveMatrixUnif, 1, GL_FALSE, theMatrix);
	glUseProgram(0);

	// Initialize vertex buffer
	float vertexData[] = {
		-0.5f,  0.2f, -1.0f, COLOR_RED,
		 0.5f,  0.2f, -1.0f, COLOR_RED,
		 0.5f,  0.0f, -0.8f, COLOR_RED,
		-0.5f,  0.0f, -0.8f, COLOR_RED,
		-0.5f,  0.0f, -0.8f, COLOR_BLUE,
		 0.5f,  0.0f, -0.8f, COLOR_BLUE,
		 0.5f, -0.2f, -1.0f, COLOR_BLUE,
		-0.5f, -0.2f, -1.0f, COLOR_BLUE
		};

	numOfVertices = 8;

	GLshort indexData[] = {
		0, 1, 2,
		0, 2, 3,

		4, 5, 6,
		4, 6, 7
	};

	int i = ARRAY_COUNT(indexData);

	// Create buffer objects
	glGenBuffers(1, &vertexBufferObject);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	glGenBuffers(1, &indexBufferObject);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indexData), indexData, GL_STATIC_DRAW);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	// Create vertex array
	glGenVertexArrays(1, &vertexArrayObject);
	glBindVertexArray(vertexArrayObject);

	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
	glEnableVertexAttribArray(0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 28, 0);
	glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 28, (void*)12);
	
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);
	glBindVertexArray(0);

	// Face culling
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glFrontFace(GL_CW);

	// Depth testing
	glEnable(GL_DEPTH_TEST);
	glDepthMask(GL_TRUE);
	glDepthFunc(GL_LEQUAL);
	glDepthRange(0.0f, 1.0f);
}


//
//
//
void Renderer::Update(float deltaTime)
{
}


//
//
//
void Renderer::Render()
{
	glClearColor(0.3f, 0.3f, 0.3f, 0.0f);
	glClearDepth(1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	//glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );

	glUseProgram(program);

	glBindVertexArray(vertexArrayObject);
	
	glUniform3f(offsetUniform, 0.0f, 0.0f, 0.0f);
	glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_SHORT, 0);

	glUniform3f(offsetUniform, 0.0f, 0.2f, 0.0f);
	glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_SHORT, 0);

	glUniform3f(offsetUniform, 0.0f, 0.4f, 0.0f);
	glDrawElementsBaseVertex(GL_TRIANGLES, 6,
		GL_UNSIGNED_SHORT, 0, 4);



	// Clean up
	glBindVertexArray(0);
	glUseProgram(0);

	// Display
	glutSwapBuffers();
}


//
//
//
void Renderer::CleanUp()
{
}


//
//
//
GLuint Renderer::LoadShader(GLenum shaderType, const std::string &fileName)
{
	// Loading shader
	std::ifstream shaderFile(fileName.c_str());

	assert(shaderFile);

	std::stringstream shaderData;
	shaderData << shaderFile.rdbuf();
	shaderFile.close();

	// Compile shader
	GLuint shader = glCreateShader(shaderType);

	// Extract string data
	const std::string &shaderString = shaderData.str();
	const char* shaderSource = shaderString.c_str();
	GLint shaderLength = (GLint)shaderString.size();

	// Compiling shader
	glShaderSource(shader, 1, (const GLchar**)&shaderSource, &shaderLength);
	glCompileShader(shader);

	GLint status;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
	if(status == GL_FALSE)
	{
		GLint infoLogLength;
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);

		GLchar* strInfoLog = new GLchar[infoLogLength+1];
		glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog);

		const char* strShaderType = NULL;
		switch(shaderType)
		{
		case GL_VERTEX_SHADER: strShaderType = "vertex"; break;
		case GL_GEOMETRY_SHADER: strShaderType = "geometry"; break;
		case GL_FRAGMENT_SHADER: strShaderType = "fragment"; break;
		}

		fprintf(stderr, "Error compiling %s shader: %s\n", strShaderType, strInfoLog);
		delete[] strInfoLog;
	}

	return shader;
}


//
//
//
void Renderer::SetWindowSize(unsigned int width, unsigned int height)
{
	glViewport(0, 0, (GLsizei)width, (GLsizei)height);
	windowWidth = width;
	windowHeight = height;
}


//
//
//
GLuint Renderer::CreateProgram(const std::vector<GLuint> &shaders)
{
	// version 2
	//GLuint prog = glutil::LinkProgram(shaders);
	//std::for_each(shaders.begin(), shaders.end(), glDeleteShader);
	//return program;

	// version 1
	GLuint program = glCreateProgram();

	for(size_t i = 0; i < shaders.size(); ++i)
		glAttachShader(program, shaders[i]);

	glLinkProgram(program);

	GLint status;
	glGetProgramiv(program, GL_LINK_STATUS, &status);
	if(status == GL_FALSE)
	{
		GLint infoLogLength;
		glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);

		GLchar* strInfoLog = new GLchar[infoLogLength+1];
		glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog);
		fprintf(stderr, "Error linking program: %s\n", strInfoLog);
		delete[] strInfoLog;
	}

	for(size_t i = 0; i < shaders.size(); ++i)
		glDetachShader(program, shaders[i]);

	return program;
}